#pragma once
#include "MathUtils.h"

namespace Alice{
    /**
     * Structure for integer points in 2-d space.
     *
     * @todo Docs: The operators need better documentation, i.e. what does it mean to divide a point?
     */
    template <typename InIntType>
    struct TIntPoint
    {
        using IntType = InIntType;
        static_assert(std::is_integral_v<IntType>, "An integer type is required.");

        union
        {
            struct
            {
                /** Holds the point's x-coordinate. */
                IntType X;

                /** Holds the point's y-coordinate. */
                IntType Y;
            };
            IntType XY[2];
        };

        /** An integer point with zeroed values. */
        static const TIntPoint ZeroValue;

        /** An integer point with INDEX_NONE values. */
        static const TIntPoint NoneValue;

        /** Default constructor (no initialization). */
        TIntPoint() = default;

        /**
         * Create and initialize a new instance with the specified coordinates.
         *
         * @param InX The x-coordinate.
         * @param InY The y-coordinate.
         */
        TIntPoint(IntType InX, IntType InY)
            : X(InX)
            , Y(InY)
        {
        }

        /**
         * Create and initialize a new instance with a single int.
         * Both X and Y will be initialized to this value
         *
         * @param InXY The x and y-coordinate.
         */
        TIntPoint(IntType InXY)
            : X(InXY)
            , Y(InXY)
        {
        }

        /**
         * Create and initialize a new instance to zero.
         *
         * @param EForceInit Force init enum
         */
        explicit TIntPoint(EForceInit)
            : X(0)
            , Y(0)
        {
        }

        /**
         * Converts to another int type. Checks that the cast will succeed.
         */
        template <typename OtherIntType>
        explicit TIntPoint(TIntPoint<OtherIntType> Other)
            : X(IntCastChecked<IntType>(Other.X))
            , Y(IntCastChecked<IntType>(Other.Y))
        {
        }

        // Workaround for clang deprecation warnings for deprecated XY member in implicitly-defined special member functions
        TIntPoint(TIntPoint&&) = default;
        TIntPoint(const TIntPoint&) = default;
        TIntPoint& operator=(TIntPoint&&) = default;
        TIntPoint& operator=(const TIntPoint&) = default;

        /**
         * Get specific component of a point.
         *
         * @param PointIndex Index of point component.
         * @return const reference to component.
         */
        const IntType& operator()(int32 PointIndex) const
        {
            return XY[PointIndex];
        }

        /**
         * Get specific component of a point.
         *
         * @param PointIndex Index of point component
         * @return reference to component.
         */
        IntType& operator()(int32 PointIndex)
        {
            return XY[PointIndex];
        }

        /**
         * Compare two points for equality.
         *
         * @param Other The other int point being compared.
         * @return true if the points are equal, false otherwise.
         */
        bool operator==(const TIntPoint& Other) const
        {
            return X == Other.X && Y == Other.Y;
        }

        /**
         * Compare two points for inequality.
         *
         * @param Other The other int point being compared.
         * @return true if the points are not equal, false otherwise.
         */
        bool operator!=(const TIntPoint& Other) const
        {
            return (X != Other.X) || (Y != Other.Y);
        }

        /**
         * Scale this point.
         *
         * @param Scale What to multiply the point by.
         * @return Reference to this point after multiplication.
         */
        TIntPoint& operator*=(IntType Scale)
        {
            X *= Scale;
            Y *= Scale;

            return *this;
        }

        /**
         * Divide this point by a scalar.
         *
         * @param Divisor What to divide the point by.
         * @return Reference to this point after division.
         */
        TIntPoint& operator/=(IntType Divisor)
        {
            X /= Divisor;
            Y /= Divisor;

            return *this;
        }

        /**
         * Add another point component-wise to this point.
         *
         * @param Other The point to add to this point.
         * @return Reference to this point after addition.
         */
        TIntPoint& operator+=(const TIntPoint& Other)
        {
            X += Other.X;
            Y += Other.Y;

            return *this;
        }

        /**
         * Multiply another point component-wise from this point.
         *
         * @param Other The point to multiply with this point.
         * @return Reference to this point after multiplication.
         */
        TIntPoint& operator*=(const TIntPoint& Other)
        {
            X *= Other.X;
            Y *= Other.Y;

            return *this;
        }

        /**
         * Subtract another point component-wise from this point.
         *
         * @param Other The point to subtract from this point.
         * @return Reference to this point after subtraction.
         */
        TIntPoint& operator-=(const TIntPoint& Other)
        {
            X -= Other.X;
            Y -= Other.Y;

            return *this;
        }

        /**
         * Divide this point component-wise by another point.
         *
         * @param Other The point to divide with.
         * @return Reference to this point after division.
         */
        TIntPoint& operator/=(const TIntPoint& Other)
        {
            X /= Other.X;
            Y /= Other.Y;

            return *this;
        }

        /**
         * Get the result of scaling on this point.
         *
         * @param Scale What to multiply the point by.
         * @return A new scaled int point.
         */
        TIntPoint operator*(IntType Scale) const
        {
            return TIntPoint(*this) *= Scale;
        }

        /**
         * Get the result of division on this point.
         *
         * @param Divisor What to divide the point by.
         * @return A new divided int point.
         */
        TIntPoint operator/(IntType Divisor) const
        {
            return TIntPoint(*this) /= Divisor;
        }

        /**
         * Get the result of addition on this point.
         *
         * @param Other The other point to add to this.
         * @return A new combined int point.
         */
        TIntPoint operator+(const TIntPoint& Other) const
        {
            return TIntPoint(*this) += Other;
        }

        /**
         * Get the result of subtraction from this point.
         *
         * @param Other The other point to subtract from this.
         * @return A new subtracted int point.
         */
        TIntPoint operator-(const TIntPoint& Other) const
        {
            return TIntPoint(*this) -= Other;
        }

        /**
         * Get the result of multiplication on this point.
         *
         * @param Other The point to multiply with this point.
         * @return A new multiplied int point
         */
        TIntPoint operator*(const TIntPoint& Other) const
        {
            return TIntPoint(*this) *= Other;
        }

        /**
         * Get the result of division on this point.
         *
         * @param Other The other point to subtract from this.
         * @return A new subtracted int point.
         */
        TIntPoint operator/(const TIntPoint& Other) const
        {
            return TIntPoint(*this) /= Other;
        }

        /**
        * Get specific component of the point.
        *
        * @param Index the index of point component
        * @return reference to component.
        */
        IntType& operator[](IntType Index)
        {
            check(Index >= 0 && Index < 2);
            return ((Index == 0) ? X : Y);
        }

        /**
        * Get specific component of the point.
        *
        * @param Index the index of point component
        * @return copy of component value.
        */
        IntType operator[](IntType Index) const
        {
            check(Index >= 0 && Index < 2);
            return ((Index == 0) ? X : Y);
        }

        /**
         * Get the component-wise min of two points.
         *
         * @see ComponentMax, GetMax
         */
        TIntPoint ComponentMin(const TIntPoint& Other) const
        {
            return TIntPoint(MathUtils::Min(X, Other.X), MathUtils::Min(Y, Other.Y));
        }

        /**
         * Get the component-wise max of two points.
         *
         * @see ComponentMin, GetMin
         */
        TIntPoint ComponentMax(const TIntPoint& Other) const
        {
            return TIntPoint(MathUtils::Max(X, Other.X), MathUtils::Max(Y, Other.Y));
        }

        /**
         * Get the larger of the point's two components.
         *
         * @return The maximum component of the point.
         * @see GetMin, Size, SizeSquared
         */
        IntType GetMax() const
        {
            return MathUtils::Max(X, Y);
        }

        /**
         * Get the smaller of the point's two components.
         *
         * @return The minimum component of the point.
         * @see GetMax, Size, SizeSquared
         */
        IntType GetMin() const
        {
            return MathUtils::Min(X, Y);
        }

        /**
         * Get the distance of this point from (0,0).
         *
         * @return The distance of this point from (0,0).
         * @see GetMax, GetMin, SizeSquared
         */
        IntType Size() const
        {
            int64 LocalX64 = (int64)X;
            int64 LocalY64 = (int64)Y;
            return IntType(MathUtils::Sqrt(double(LocalX64 * LocalX64 + LocalY64 * LocalY64)));
        }

        /**
         * Get the squared distance of this point from (0,0).
         *
         * @return The squared distance of this point from (0,0).
         * @see GetMax, GetMin, Size
         */
        IntType SizeSquared() const
        {
            return X * X + Y * Y;
        }
        /**
         * Divide an int point and round up the result.
         *
         * @param lhs The int point being divided.
         * @param Divisor What to divide the int point by.
         * @return A new divided int point.
         * @see DivideAndRoundDown
         */
        static TIntPoint DivideAndRoundUp(TIntPoint lhs, IntType Divisor)
        {
            return TIntPoint(MathUtils::DivideAndRoundUp(lhs.X, Divisor), MathUtils::DivideAndRoundUp(lhs.Y, Divisor));
        }

        static TIntPoint DivideAndRoundUp(TIntPoint lhs, TIntPoint Divisor)
        {
            return TIntPoint(MathUtils::DivideAndRoundUp(lhs.X, Divisor.X), MathUtils::DivideAndRoundUp(lhs.Y, Divisor.Y));
        }

        /**
         * Divide an int point and round down the result.
         *
         * @param lhs The int point being divided.
         * @param Divisor What to divide the int point by.
         * @return A new divided int point.
         * @see DivideAndRoundUp
         */
        static TIntPoint DivideAndRoundDown(TIntPoint lhs, IntType Divisor)
        {
            return TIntPoint(MathUtils::DivideAndRoundDown(lhs.X, Divisor), MathUtils::DivideAndRoundDown(lhs.Y, Divisor));
        }

        static TIntPoint DivideAndRoundDown(TIntPoint lhs, TIntPoint Divisor)
        {
            return TIntPoint(MathUtils::DivideAndRoundDown(lhs.X, Divisor.X), MathUtils::DivideAndRoundDown(lhs.Y, Divisor.Y));
        }

        /**
         * Get number of components point has.
         *
         * @return number of components point has.
         */
        static int32 Num()
        {
            return 2;
        }
    };
    using Int32Point=TIntPoint<int32>;
    using IntPoint=Int32Point;
}